Skip to content

RFC: Clone with v2 #18747

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open

RFC: Clone with v2 #18747

wants to merge 12 commits into from

Conversation

TimWolla
Copy link
Member

@TimWolla TimWolla commented Jun 3, 2025

RFC: https://wiki.php.net/rfc/clone_with_v2

see TimWolla#6 for a preliminary review.

@TimWolla TimWolla force-pushed the clone-with branch 2 times, most recently from de238ac to b99daf4 Compare June 30, 2025 10:33
@TimWolla TimWolla force-pushed the clone-with branch 2 times, most recently from 78b7ac4 to 7e88344 Compare July 7, 2025 10:24
@TimWolla TimWolla marked this pull request as ready for review July 7, 2025 10:27
@TimWolla TimWolla requested review from iluuu1994 and nielsdos July 7, 2025 10:28
Copy link
Member

@iluuu1994 iluuu1994 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that I did not (yet) carefully look at all tests.

{
zend_object *new_object;

/* Compatibility with code that only overrides clone_obj. */
if (UNEXPECTED(old_object->handlers->clone_obj != zend_objects_clone_obj)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this problematic for overridden clone_obj that reuse zend_objects_clone_obj? In that case old_object->handlers->clone_obj != zend_objects_clone_obj would hit, and we'd recurse infinitely.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. There are none in php-src and generally speaking it seems unlikely that there are any, since when there is a need to overwrite clone there likely also is a need to overwrite create (which explicitly isn't supported):

/* assume that create isn't overwritten, so when clone depends on the

  • overwritten one then it must itself be overwritten */

Most of the custom clone handlers just use zend_objects_clone_members().


This PR tries to minimize the disruption for existing code, but if there is an external extension that does what you suggest, then it's easy enough for the author to adjust it to add an explicit clone_with handler.

Copy link
Member

@iluuu1994 iluuu1994 Jul 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, you could do the inverse: Keep clone_obj as is, call it whenever properties are not provided or when the array is empty. When it isn't, clone_obj_with is invoked, which can call clone_obj to clone the object, and adjust the properties afterwards. This way, an extension could simply continue to provide clone_obj and the property part would just work. This approach seems more logical to me.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

which can call clone_obj to clone the object, and adjust the properties afterwards

This is likely dangerous, since some internal classes might expect their (readonly) properties not to change, which leads to some C structures related to a property not being properly adjusted. Random\Randomizer is currently not cloneable at all, but it stores both a public readonly \Random\Engine $engine property exposed to userland and also the raw “engine pointer” for more efficient access.

The implementation also prefers the clone_with handler, since I would like to see the old clone_obj hook to go away in the future to remove the duplication.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is likely dangerous, since some internal classes might expect their (readonly) properties not to change

Can this already be broken with __clone in a user-declared sub-class? I can't see cases other than enums preventing the declaration of __clone. The property would need to be private(set) to prevent this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this already be broken with __clone in a user-declared sub-class?

Ah, yes. Implicit protected(set), of course. I've implemented your suggestion in 26ae13b. While it allowed to remove quite a bit of the custom logic, I feel that overall the code is in a worse shape with the removal of “clone with” being a first-class citizen.

Documenting the small break to the internal API that you found would be preferable to me (thus dropping that commit).

What do you think?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, the new commit looks much more logical to me, and it should work automatically for extensions that already customize clone. Right now I'm failing to see the downsides, but maybe some others can chime in.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now I'm failing to see the downsides,

For me it made sense to have the “clone-with” logic as part of the zend_objects_clone_members to have all the cloning logic in a single place, particularly for classes that do need a custom clone_with and would have to reimplement it with the new commit.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with Ilija on this. I like the current code more.
You could split off the property update logic to a separate helper so that part can be reused? I'd think that either:

  • A custom class may want to do some validation and call such a helper
  • A custom class has entire custom logic to deal with it

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay. I've also gotten private feedback that the new version is preferable, so I'll leave it like that. Thanks!

GC_ADDREF(new_object);

/* Unlock readonly properties once more. */
if (ZEND_CLASS_HAS_READONLY_PROPS(new_object->ce)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If write_property in the loop below triggers a hook or magic setter, then you can set readonly properties that were not included in withProperties. I don't know if this is desired or not.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will need to give this some thought / run some tests regarding the current behavior of __clone() with hooks / magics and discuss this with @edorian.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We discussed this and consider this to be desired: A property hook is added by the class itself and thus the class author has full control over what the hook does.

If updating some readonly property as part of the assignment to another property is required to keep an object in a consistent state, then it should still be allowed to update that property with clone-with. Of course trying to also assign that readonly property with a value given in clone-with will then fail, but this is consistent with regular assignments when the readonly property is still uninitialized.

I've added Zend/tests/clone/clone_with_012.phpt to test this behavior.

zend_ulong num_key;
zend_string *key;
zval *val;
ZEND_HASH_FOREACH_KEY_VAL(properties, num_key, key, val) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The following code will break:

$lol = 'I love references';
$x = new stdClass;
$y = clone($x, ['x' => &$lol]);
var_dump($y);

Copy link
Member Author

@TimWolla TimWolla Jul 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Behavior with references is not clearly specified in the RFC, but following the showcased “desugaring” of:

$cloned = clone $object;
foreach ($withProperties as $key => $value) {
    $cloned->{$key} = $value;
}
return $cloned;

would mean that unwrapping references would be the correct solution. Alternatively, we could also throw an Error that references are unsupported and defer the question to the future. What do you think @edorian?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We decided to make references an error for now and will follow-up with another RFC if someone complains / if the need arises.

Copy link
Member

@iluuu1994 iluuu1994 Jul 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, I also think unwrapping the references is the correct solution, as this is what happens for normal assignments. You can easily encounter references even if they are unique, e.g. by looping over the array by-ref that is passed to clone.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also think unwrapping the references is the correct solution, as this is what happens for normal assignments.

Passing a reference within the array might also be an intentional attempt to make the property a reference. That's why we opted not to make a decision for now, we expect the most common use-case for clone-with to be an “array literal”.

Copy link
Member

@iluuu1994 iluuu1994 Jul 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be reasonable to at least unwrap references if they have RC=1, as these are most-likely accidental. That's generally how we treat RC=1 references. The engine is supposed to treat them as normal values, making the exact moment they are unwrapped irrelevant. E.g. var_dump() hides the fact this array contains a reference. https://3v4l.org/Pp48f#vnull

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be reasonable to at least unwrap references if they have RC=1, as these are most-likely accidental.

Okay, will look into that tomorrow.

{
zend_object *new_object;

/* Compatibility with code that only overrides clone_obj. */
if (UNEXPECTED(old_object->handlers->clone_obj != zend_objects_clone_obj)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with Ilija on this. I like the current code more.
You could split off the property update logic to a separate helper so that part can be reused? I'd think that either:

  • A custom class may want to do some validation and call such a helper
  • A custom class has entire custom logic to deal with it

TimWolla added a commit to TimWolla/php-src that referenced this pull request Jul 16, 2025
TimWolla added a commit that referenced this pull request Jul 16, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants